﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Data.Linq.Mapping;

namespace KamLib
{


    public static class LinqHelper
    {
        public static IEnumerable<T> AddEmpty<T>(this IEnumerable<T> target) where T : class
        {
            return (new T[] { null }).Concat(target);
        }

        public static IQueryable<T> AddEmpty<T>(this IQueryable<T> target) where T : class
        {
            return (new T[] { null }).AsQueryable().Concat(target);
        }


        #region Filter

        public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> source, IDictionary<string, object> filters)
        {
            if (filters == null || filters.Count == 0) return source;
            var srcType = typeof(TSource);
            var param = Expression.Parameter(srcType, "a");
            Expression exp = null;
            foreach (var value in filters.Reverse())
            {
                var member = srcType.GetPropertyOrField(value.Key);
                Expression prop;
                Type ptype;

                if (member is PropertyInfo)
                {
                    prop = Expression.Property(param, value.Key);
                    ptype = ((PropertyInfo)member).PropertyType;
                }
                else
                {
                    prop = Expression.Field(param, value.Key);
                    ptype = ((FieldInfo)member).FieldType;
                }
                var eq = Expression.Equal(prop, Expression.Constant(value.Value.To(ptype), ptype));
                exp = exp == null ? eq : Expression.AndAlso(eq, exp);
            }
            return exp == null ? source : source.Where(Expression.Lambda<Func<TSource, bool>>(exp, param));
        }

        #endregion


        #region Select

        public static Expression<Func<TSource, TResult>> SelectBuild<TSource, TResult>(
            Expression<Func<TSource, TResult>> init = null
        )
        {
            var srcType = typeof(TSource);
            var resultType = typeof(TResult);
            var pa = init != null ? init.Parameters[0] : Expression.Parameter(srcType, "a");

            var resultProps = resultType.GetPropertiesAndFields();
            var members = new List<MemberBinding>(resultProps.Length);

            var bindings = init != null ? ((MemberInitExpression)init.Body).Bindings : null;

            foreach (var prop in resultProps)
            {
                try
                {
                    var prop1 = prop;
                    // пропускает свойства, уже объявленные в init
                    if (bindings != null && bindings.OfType<MemberAssignment>().FirstOrDefault(a => a.Member.Name == prop1.Name) != null)
                        continue;


                    foreach (var convert in Attribute.GetCustomAttributes(prop, typeof(ConvertAttribute))
                        .OfType<ConvertAttribute>().Where(a => a.SourceField != null))
                    {
                        var srcProp_ = srcType.GetPropertyOrField(convert.SourceField);
                        if (srcProp_ == null || srcProp_.GetCustomAttributes(typeof(ColumnAttribute), false).Length == 0) continue;

                        var expr = convert.InitBind(pa, srcProp_);
                        if (expr != null)
                            members.Add(Expression.Bind(prop, expr));
                    }


                    var srcProp = srcType.GetPropertyOrField(prop.Name);

                    if (srcProp != null)
                    {
                        if (srcProp.GetCustomAttributes(typeof(ColumnAttribute), false).Length == 0) continue;
                        //if (srcProp == null || srcProp.GetCustomAttributes(typeof(DataMemberAttribute), false).Length == 0) continue;

                        Expression expr = null;

                        foreach (var convert in Attribute.GetCustomAttributes(prop, typeof(ConvertAttribute))
                            .OfType<ConvertAttribute>().Where(a => a.SourceField == null))
                        {
                            expr = convert.InitBind(pa, srcProp);
                            if (expr != null) break;
                        }

                        if (expr == null)
                        {
                            expr = Expression.PropertyOrField(pa, prop.Name);
                            var propType = prop.ResultType();
                            if (propType != srcProp.ResultType())
                            //if (!srcProp.ResultType().IsAssignableFrom(propType))
                                expr = Expression.Convert(expr, propType);
                        }

                        members.Add(Expression.Bind(prop, expr));
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("SelectBuild: property '" + prop.Name + "'. \r\n" + ex.FullMessage());
                }
            }
            if (bindings != null)
                members.AddRange(bindings);

            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.MemberInit(Expression.New(resultType), members), pa
            );
        }

        public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source)
        {
            return source.Select(SelectBuild<TSource, TResult>());
        }

        public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, ref Expression<Func<TSource, TResult>> expr, Expression<Func<TSource, TResult>> init = null)
        {
            return source.Select(expr ?? (expr = SelectBuild(init)));
        }

        #endregion


        #region Sort

        public static IOrderedQueryable<TSource> Sort<TSource, TSort>(this IQueryable<TSource> source,
            MemberInfo sortField, bool sortDir, bool useOrderThen)
        {
            var param = Expression.Parameter(typeof(TSource), "a");
            var sortExpression = Expression.Lambda<Func<TSource, TSort>>(
                Expression.PropertyOrField(param, sortField.Name),
                param
            );

            IOrderedQueryable<TSource> result;
            if (useOrderThen && source is IOrderedQueryable<TSource>)
                result = sortDir
                    ? ((IOrderedQueryable<TSource>)source).ThenBy(sortExpression)
                    : ((IOrderedQueryable<TSource>)source).ThenByDescending(sortExpression);
            else
                result = sortDir
                    ? source.OrderBy(sortExpression)
                    : source.OrderByDescending(sortExpression);
            return result;
        }


        public static IOrderedQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source,
            MemberInfo sortProp, bool sortDir, bool useOrderThen)
        {
            var sortType = sortProp.ResultType();

            if (sortType == typeof(string))
                return source.Sort<TSource, string>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(int))
                return source.Sort<TSource, int>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(int?))
                return source.Sort<TSource, int?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(decimal))
                return source.Sort<TSource, decimal>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(decimal?))
                return source.Sort<TSource, decimal?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(double))
                return source.Sort<TSource, double>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(double?))
                return source.Sort<TSource, double?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(DateTime))
                return source.Sort<TSource, DateTime>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(DateTime?))
                return source.Sort<TSource, DateTime?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(bool))
                return source.Sort<TSource, bool>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(bool?))
                return source.Sort<TSource, bool?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(float))
                return source.Sort<TSource, float>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(float?))
                return source.Sort<TSource, float?>(sortProp, sortDir, useOrderThen);

            if (sortType == typeof(long))
                return source.Sort<TSource, long>(sortProp, sortDir, useOrderThen);
            if (sortType == typeof(long?))
                return source.Sort<TSource, long?>(sortProp, sortDir, useOrderThen);

            throw new ArgumentException("Sort: The type " + sortType.Name + " of property/field " + sortProp.Name + " not support", "sortProp");
        }

        public static IQueryable<TSource> Sort<TSource>(this IQueryable<TSource> source,
            string sortField, bool sortDir, bool useOrderThen)
        {
            if (string.IsNullOrEmpty(sortField))
                return source;
            var sortProp = typeof(TSource).GetPropertyOrField(sortField);
            if (sortProp == null)
                throw new ArgumentException("Sort: The field " + sortField + " is not exist in source", "sortField");

            return source.Sort(sortProp, sortDir, useOrderThen);
        }

        public static IEnumerable<TSource> Sort<TSource>(this IEnumerable<TSource> source,
            MemberInfo sortField, bool sortDir, bool useOrderThen)
        {
            Func<TSource, object> keySelector = a => sortField.GetValue(a, null);

            IOrderedEnumerable<TSource> result;
            if (useOrderThen && source is IOrderedEnumerable<TSource>)
                result = sortDir
                    ? ((IOrderedEnumerable<TSource>)source).ThenBy(keySelector)
                    : ((IOrderedEnumerable<TSource>)source).ThenByDescending(keySelector);
            else
                result = sortDir
                    ? source.OrderBy(keySelector)
                    : source.OrderByDescending(keySelector);
            return result;
        }


        public static IEnumerable<TSource> Sort<TSource>(this IEnumerable<TSource> source,
            string sortField, bool sortDir, bool useOrderThen)
        {
            if (string.IsNullOrEmpty(sortField))
                return source;
            var sortProp = typeof(TSource).GetPropertyOrField(sortField);
            if (sortProp == null)
                throw new ArgumentException("Sort: The field " + sortField + " is not exist in source", "sortField");

            return source.Sort(sortProp, sortDir, useOrderThen);
        }

        #endregion


        #region Limit

        public static IQueryable<TSource> Limit<TSource>(this IQueryable<TSource> source, int start, int limit)
        {
            if (source is IOrderedQueryable<TSource> && start >= 0 && limit > 0)
                return source.Skip(start).Take(limit);
            return source;
        }

        #endregion

    }
}